package pers.qianyu.diytomcat.catalina;

import cn.hutool.core.date.*;
import cn.hutool.core.io.*;
import cn.hutool.core.util.*;
import cn.hutool.log.*;
import org.apache.jasper.*;
import org.apache.jasper.compiler.*;
import org.jsoup.*;
import org.jsoup.nodes.*;
import org.jsoup.select.*;
import pers.qianyu.diytomcat.classloader.*;
import pers.qianyu.diytomcat.exception.*;
import pers.qianyu.diytomcat.http.*;
import pers.qianyu.diytomcat.http.StandardServletConfig;
import pers.qianyu.diytomcat.util.*;
import pers.qianyu.diytomcat.watcher.*;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.util.stream.*;

/**
 * @author mizzle rain
 * @date 2021-01-07 15:09
 */
public class Context {
    private String path;
    private String docBase;
    private boolean reloadable;
    private Host host;

    private File contextWebXmlFile;
    private Map<String, String> url2ServletClass;
    private Map<String, String> url2ServletName;
    private Map<String, String> servletName2ServeltClass;
    private Map<String, String> servletClass2ServletName;

    private Map<String, Set<String>> url2FilterClasses;
    private Map<String, Set<String>> url2FilterNames;
    private Map<String, String> filterName2FilterClass;
    private Map<String, String> filterClass2FilterName;
    private Map<String, Map<String, String>> filterClass2InitParameters;

    private WebappClassLoader webappClassLoader;
    private ContextFileChangeWatcher contextFileChangeWatcher;
    private ServletContext servletContext;
    private Map<Class<?>, HttpServlet> servletPool;
    private Map<String, Map<String, String>> servletClassName2InitParameters;
    private List<String> loadOnStartupServletClassNames;

    private Map<String, Filter> filterPool;
    private List<ServletContextListener> listeners;

    public Context(String path, String docBase, Host host, boolean reloadable) {
        this.path = path;
        this.docBase = docBase;
        this.reloadable = reloadable;
        this.host = host;
        contextWebXmlFile = new File(docBase, ContextXmlUtil.getWatchedResource());
        url2ServletClass = new HashMap<>();
        url2ServletName = new HashMap<>();
        servletName2ServeltClass = new HashMap<>();
        servletClass2ServletName = new HashMap<>();

        url2FilterClasses = new HashMap<>();
        url2FilterNames = new HashMap<>();
        filterName2FilterClass = new HashMap<>();
        filterClass2FilterName = new HashMap<>();
        filterClass2InitParameters = new HashMap<>();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        webappClassLoader = new WebappClassLoader(docBase, classLoader);
        servletContext = new ApplicationContext(this);
        servletPool = new HashMap<>();
        servletClassName2InitParameters = new HashMap<>();
        loadOnStartupServletClassNames = new ArrayList<>();
        filterPool = new HashMap<>();
        listeners = new ArrayList<>();
        deploy();
        initFilters();
    }

    private void deploy() {
        TimeInterval timer = DateUtil.timer();
        Log log = LogFactory.get();
        log.info("Deploying web application directory {}", docBase);
        JspC c = new JspC();
        new JspRuntimeContext(servletContext, c);
        init();
        if (reloadable) {
            contextFileChangeWatcher = new ContextFileChangeWatcher(this);
            contextFileChangeWatcher.start();
        }
        log.info("Deployment of web application directory {} has finished in {} ms", docBase, timer.intervalMs());
    }

    public void stop() {
        contextFileChangeWatcher.stop();
        webappClassLoader.stop();
        destroyServlets();
        fireEvent("destroy");
    }

    private void init() {
        if (!contextWebXmlFile.exists()) {
            return;
        }
        try {
            checkDuplicated();
        } catch (WebConfigDuplicatedException e) {
            e.printStackTrace();
            return;
        }
        String xml = FileUtil.readUtf8String(contextWebXmlFile);
        Document document = Jsoup.parse(xml);
        parseServletMapping(document);
        parseServletInitParams(document);
        parseLoadOnStartUp(document);
        parseFilterMapping(document);
        parseFilterInitParameters(document);
        parseListeners(document);
        handleLoadOnStartup();
        fireEvent("init");
    }

    private void parseLoadOnStartUp(Document document) {
        Elements elements = document.select("load-on-startup");
        for (Element element : elements) {
            String servletClassName = element.parent()
                    .select("servlet-class").first().text();
            loadOnStartupServletClassNames.add(servletClassName);
        }
    }

    private void parseServletInitParams(Document document) {
        Elements elements = document.select("servlet");
        for (Element element : elements) {
            String servletClass = element.select("servlet-class").first().text();
            Elements es = element.select("init-param");
            HashMap<String, String> map = new HashMap<>();
            for (Element e : es) {
                String name = e.select("param-name").first().text();
                String value = e.select("param-value").first().text();
                map.put(name, value);
            }
            servletClassName2InitParameters.put(servletClass, map);
        }
    }

    private void parseServletMapping(Document document) {
        Elements elements = document.select("servlet");
        for (Element element : elements) {
            String servletName = element.select("servlet-name").text();
            String servletClass = element.select("servlet-class").text();
            servletName2ServeltClass.put(servletName, servletClass);
            servletClass2ServletName.put(servletClass, servletName);
        }
        elements = document.select("servlet-mapping");
        for (Element element : elements) {
            String servletName = element.select("servlet-name").text();
            String url = element.select("url-pattern").text();
            url2ServletName.put(url, servletName);
            url2ServletClass.put(url, servletName2ServeltClass.get(servletName));
        }
    }

    private void parseFilterMapping(Document document) {
        Elements elements = document.select("filter");
        for (Element element : elements) {
            String filterName = element.selectFirst("filter-name").text();
            String filterClass = element.selectFirst("filter-class").text();
            filterName2FilterClass.put(filterName, filterClass);
            filterClass2FilterName.put(filterClass, filterName);
        }
        elements = document.select("filter-mapping");
        for (Element element : elements) {
            String filterName = element.selectFirst("filter-name").text();
            String urlPattern = element.selectFirst("url-pattern").text();

            Set<String> set1 = url2FilterNames.getOrDefault(urlPattern, new HashSet<>());
            set1.add(filterName);
            url2FilterNames.put(urlPattern, set1);

            String filterClass = filterName2FilterClass.get(filterName);
            Set<String> set2 = url2FilterClasses.getOrDefault(urlPattern, new HashSet<>());
            set2.add(filterClass);
            url2FilterClasses.put(urlPattern, set2);
        }
    }

    private void parseFilterInitParameters(Document document) {
        Elements elements = document.select("filter");
        for (Element element : elements) {
            Elements initParamsNodes = element.select("init-param");
            if (initParamsNodes.isEmpty()) {
                continue;
            }
            String filterClass = element.select("filter-class").first().text();
            for (Element initParamsNode : initParamsNodes) {
                String name = initParamsNode.select("param-name").first().text();
                String value = initParamsNode.select("param-value").first().text();
                Map<String, String> map = filterClass2InitParameters.getOrDefault(filterClass, new HashMap<>());
                map.put(name, value);
                filterClass2InitParameters.put(filterClass, map);
            }
        }
    }

    private void parseListeners(Document document) {
        try {
            Elements elements = document.select("listener");
            for (Element element : elements) {
                String listenerClass = element.select("listener-class").first().text();
                Class<?> clazz = getWebappClassLoader().loadClass(listenerClass);
                ServletContextListener listener = (ServletContextListener) clazz.newInstance();
                addListener(listener);
            }
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }

    private void fireEvent(String eventName) {
        ServletContextEvent event = new ServletContextEvent(servletContext);
        for (ServletContextListener listener : listeners) {
            if ("init".equals(eventName)) {
                listener.contextInitialized(event);
            }
            if ("destroy".equals(eventName)) {
                listener.contextDestroyed(event);
            }
        }
    }

    private void checkDuplicated() throws WebConfigDuplicatedException {
        String xml = FileUtil.readUtf8String(contextWebXmlFile);
        Document d = Jsoup.parse(xml);
        checkDuplicated(d, "servlet servlet-name", "Servlet 名称重复，请保持其唯一性：{}");
        checkDuplicated(d, "servlet-mapping servlet-name", "Servlet 名称重复，请保持其唯一性：{}");
        checkDuplicated(d, "servlet servlet-class", "Servlet 类名重复，请保持其唯一性：{}");
        checkDuplicated(d, "servlet-mapping url-pattern", "Servlet url重复，请保持其唯一性：{}");
    }

    private void checkDuplicated(Document document, String mapping, String desc) throws WebConfigDuplicatedException {
        Elements elements = document.select(mapping);
        List<String> list = elements.stream()
                .map(Element::text)
                .sorted()
                .collect(Collectors.toList());
        for (int i = 0; i < list.size() - 1; i++) {
            String cur = list.get(i);
            String next = list.get(i + 1);
            if (Objects.equals(cur, next)) {
                throw new WebConfigDuplicatedException(StrUtil.format(desc, cur));
            }
        }
    }

    public String getServletClassName(String url) {
        return url2ServletClass.get(url);
    }

    public void reload() {
        host.reload(this);
    }

    public synchronized HttpServlet getServlet(Class<?> clazz) throws ServletException {
        HttpServlet httpServlet = servletPool.get(clazz);
        if (Objects.isNull(httpServlet)) {
            httpServlet = (HttpServlet) ReflectUtil.newInstance(clazz);
            ServletContext servletContext = getServletContext();
            Map<String, String> initParameters = servletClassName2InitParameters.get(clazz.getName());
            StandardServletConfig servletConfig = new StandardServletConfig(servletContext, servletClass2ServletName.get(clazz.getName()), initParameters);
            httpServlet.init(servletConfig);
            servletPool.put(clazz, httpServlet);
        }
        return httpServlet;
    }

    private void destroyServlets() {
        servletPool.forEach((k, v) -> v.destroy());
    }

    private void handleLoadOnStartup() {
        for (String className : loadOnStartupServletClassNames) {
            try {
                Class<?> clazz = webappClassLoader.loadClass(className);
                getServlet(clazz);
            } catch (ClassNotFoundException | ServletException e) {
                e.printStackTrace();
            }
        }
    }

    private void initFilters() {
        Set<String> classNames = filterClass2FilterName.keySet();
        for (String className : classNames) {
            try {
                Class<?> clazz = getWebappClassLoader().loadClass(className);
                String filterName = filterClass2FilterName.get(className);
                Map<String, String> initParameters = filterClass2InitParameters.get(className);
                StandardFilterConfig filterConfig = new StandardFilterConfig(filterName, getServletContext(), initParameters);
                Filter filter = filterPool.get(className);
                if (Objects.isNull(filter)) {
                    filter = (Filter) ReflectUtil.newInstance(clazz);
                    filter.init(filterConfig);
                    filterPool.put(className, filter);
                }
            } catch (ClassNotFoundException | ServletException e) {
                e.printStackTrace();
            }
        }
    }

    private boolean isMatch(String uri, String pattern) {
        if (StrUtil.equals(pattern, "/*")) {
            return true;
        }
        if (StrUtil.equals(uri, pattern)) {
            return true;
        }
        if (StrUtil.startWith(pattern, "/*.")) {
            String patternExt = StrUtil.subAfter(pattern, ".", false);
            String ext = StrUtil.subAfter(uri, ".", false);
            if (Objects.equals(ext, patternExt)) {
                return true;
            }
        }
        return false;
    }

    public List<Filter> getMatchedFilter(String uri) {
        Set<String> patterns = url2FilterClasses.keySet();
        HashSet<String> matchedPatterns = new HashSet<>();
        for (String pattern : patterns) {
            if (isMatch(uri, pattern)) {
                matchedPatterns.add(pattern);
            }
        }
        HashSet<String> classNames = new HashSet<>();
        for (String url : matchedPatterns) {
            classNames.addAll(url2FilterClasses.get(url));
        }
        ArrayList<Filter> filters = new ArrayList<>();
        for (String className : classNames) {
            Filter filter = filterPool.get(className);
            if (Objects.nonNull(filter)) {
                filters.add(filter);
            }
        }
        return filters;
    }

    public void addListener(ServletContextListener listener) {
        listeners.add(listener);
    }

    /************ automatic generated ***************/
    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getDocBase() {
        return docBase;
    }

    public void setDocBase(String docBase) {
        this.docBase = docBase;
    }

    public WebappClassLoader getWebappClassLoader() {
        return webappClassLoader;
    }

    public void setWebappClassLoader(WebappClassLoader webappClassLoader) {
        this.webappClassLoader = webappClassLoader;
    }

    public boolean isReloadable() {
        return reloadable;
    }

    public void setReloadable(boolean reloadable) {
        this.reloadable = reloadable;
    }

    public Host getHost() {
        return host;
    }

    public void setHost(Host host) {
        this.host = host;
    }

    public ServletContext getServletContext() {
        return servletContext;
    }

    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

}
